<?php

namespace EasyWaf;

class Check
{
    /**
     * wafConfig
     */
    protected $wafConfig = [];

    /**
     * getFilter
     * @var string
     */
    protected $getFilter = "\\<.+javascript:window\\[.{1}\\\\x|\.\.\/|\.\/|invokefunction|call_user_func|call_user_func_array|<.*=(&#\\d+?;?)+?>|<.*(data|src)=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[a-z]+?\\b[^>]*?\\bon([a-z]{4,})\s*?=|^\\+\\/v(8|9)|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

    /**
     * postFilter
     * @var string
     */
    protected $postFilter = "<.*=(&#\\d+?;?)+?>|<.*data=data:text\\/html.*>|\\b(alert\\(|confirm\\(|expression\\(|prompt\\(|benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\()|<[^>]*?\\b(onerror|onmousemove|onload|onclick|onmouseover)\\b|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

    /**
     * cookieFilter
     * @var string
     */
    protected $cookieFilter = "benchmark\s*?\(.*\)|sleep\s*?\(.*\)|load_file\s*?\\(|\\b(and|or)\\b\\s*?([\\(\\)'\"\\d]+?=[\\(\\)'\"\\d]+?|[\\(\\)'\"a-zA-Z]+?=[\\(\\)'\"a-zA-Z]+?|>|<|\s+?[\\w]+?\\s+?\\bin\\b\\s*?\(|\\blike\\b\\s+?[\"'])|\\/\\*.*\\*\\/|<\\s*script\\b|\\bEXEC\\b|UNION.+?SELECT\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)|UPDATE\s*(\(.+\)\s*|@{1,2}.+?\s*|\s+?.+?|(`|'|\").*?(`|'|\")\s*)SET|INSERT\\s+INTO.+?VALUES|(SELECT|DELETE)@{0,2}(\\(.+\\)|\\s+?.+?\\s+?|(`|'|\").*?(`|'|\"))FROM(\\(.+\\)|\\s+?.+?|(`|'|\").*?(`|'|\"))|(CREATE|ALTER|DROP|TRUNCATE)\\s+(TABLE|DATABASE)";

    /**
     * userAgentFilter
     * @var string
     */
    protected $userAgentFilter = "BLEXBot|mj12bot|HttpClient|DataForSeoBot|serpstatbot|meta-externalagent|ClaudeBot|facebookcatalog|GPTBot|SkyworkSpider|Qwantify|DotBot|AhrefsBot|FriendlyCrawler|msnbot|YodaoBot|PanguBot|iaskspider|SemrushBot|facebookexternalhit|Amazonbot|python|httrack|apache-httpclient|harvest|audit|dirbuster|pangolin|nmap|sqln|hydra|parser|libwww|bbbike|sqlmap|w3af|owasp|nikto|fimap|havij|zmeu|babykrokodil|netsparker|httperf|sf|Go-http-client";

    /**
     * __construct
     */
    public function __construct()
    {
        try {
            $this->wafConfig = Config::get(Enums::WafConfig);
            $this->checkCc();
            $this->checkGet();
            $this->checkPost();
            $this->checkCookie();
            $this->checkUserAgent();
            $this->checkProxyRequest();
            $this->checkRequestMethod();
        } catch (Exception $exception) {
            $businessCode = $exception->getCode();
            $illegalReqCallback = $this->wafConfig['illegalReqCallback'] ?? false;
            if ($illegalReqCallback) {
                $illegalReqCallback($businessCode);
            }
            http_response_code(403);
            $tpl = __DIR__ . '/tpl/stop.tpl';
            $message = file_get_contents($tpl);
            $message = str_replace('error', $exception->getMessage(), $message);
            echo $message;
            exit();
        }
    }

    /**
     * checkRequestMethod
     * @throws Exception
     */
    private function checkRequestMethod()
    {
        $allowReqMethod = $this->wafConfig['checkRequestMethod'] ?? [];
        if ($allowReqMethod) {
            $reqMethod = empty($_SERVER['REQUEST_METHOD']) ? '' : $_SERVER['REQUEST_METHOD'];
            $reqMethod = strtolower($reqMethod);
            $isIn = false;
            foreach ($allowReqMethod as $method) {
                if ($reqMethod == strtolower($method)) {
                    $isIn = true;
                    break;
                }
            }
            if (!$isIn) {
                $debug = $this->wafConfig['debug'] ?? false;
                $message = $debug ? "请求方法" . $reqMethod . "未允许" : "请求未授权";
                throw new Exception($message, 108);
            }
        }
    }

    /**
     * checkCc
     * @throws Exception
     */
    private function checkCc()
    {
        $debug = $this->wafConfig['debug'] ?? false;
        $checkCcConfig = $this->wafConfig['checkCc'] ?? false;
        $checkCcEnable = $checkCcConfig['enable'] ?? false;
        $checkCcRedis = $this->wafConfig['redis'] ?? false;
        $checkCcReqTime = $checkCcConfig['reqTime'] ?? false;
        $checkCcReqCount = $checkCcConfig['reqCount'] ?? false;
        $checkCCReqDayMaxCount = $checkCcConfig['reqDayMaxCount'] ?? false;
        $enableSeoIpCheck = $checkCcConfig['enableSeoIpCheck'] ?? false;
        $enableBehaviorCheck = $checkCcConfig['enableBehaviorCheck'] ?? false;
        if ($checkCcEnable && $checkCcRedis && $checkCcReqTime && $checkCcReqCount) {

            // 请求IP
            $ip = Utils::getClientRealIp();

            // IP在白名单,跳过
            $ipWhiteKey = Utils::getIpCacheKey($ip, Enums::WafIpWhiteListKey);
            if ($checkCcRedis->get($ipWhiteKey)) {
                return;
            }

            // 如果IP在黑名单,拦截
            $ipBlackKey = Utils::getIpCacheKey($ip, Enums::WafIpBlackListKey);
            if ($checkCcRedis->get($ipBlackKey)) {
                $message = "您的请求IP" . $ip . "已封禁,请更换IP访问";
                throw new Exception($message, 109);
            }

            // SEO爬虫特征防止误伤,使用cli进程单独检查
            if ($enableSeoIpCheck && Utils::isSearchEnginesSpiderFeature()) {
                // 加入检测队列
                $checkCcRedis->lPush(Enums::WafCheckIpDnsQueueKey, $ip);
                return;
            }

            // 检查CC-攻击
            $checkCcKey = md5(__CLASS__ . $ip);
            $check = $checkCcRedis->exists($checkCcKey);
            $checkIpBehaviorKey = Utils::getIpCacheKey($ip, Enums::WafCheckIpBehaviorKey);
            if ($check) {
                $checkCcRedis->incr($checkCcKey);
                $count = $checkCcRedis->get($checkCcKey);
                if ($count > $checkCcReqCount) {
                    $message = $debug ? $checkCcReqTime . "秒内请求超过" . $checkCcReqCount . '次' : "请求过于频繁";
                    throw new Exception($message, 100);
                }
                if ($enableBehaviorCheck) {
                    if (Utils::getCookie($checkIpBehaviorKey) != 1) {
                        $message = "您的请求IP请求行为异常,请更换IP访问";
                        throw new Exception($message, 110);
                    }
                }
            } else {
                $checkCcRedis->incr($checkCcKey);
                $checkCcRedis->expire($checkCcKey, $checkCcReqTime);
                if ($enableBehaviorCheck) {
                    Utils::setCookie($checkIpBehaviorKey, 1, time() + 86400 * 365, '/');
                }
            }

            // 检查每日请求次数限制
            if ($checkCCReqDayMaxCount > 0) {
                $today = date('Ymd');
                $checkCcCountKey = md5(__CLASS__ . $ip . $today);
                // 获取当前 IP 当天的请求次数
                $requests = $checkCcRedis->get($checkCcCountKey);
                if ($requests === false) {
                    $requests = 1;
                    $checkCcRedis->set($checkCcCountKey, 1);
                    $checkCcRedis->expireAt($checkCcCountKey, strtotime('tomorrow'));
                } else {
                    $requests++;
                    $checkCcRedis->incr($checkCcCountKey);
                }
                if ($requests > $checkCCReqDayMaxCount) {
                    $message = $debug ? "请求次数超过当天最大值" . $checkCCReqDayMaxCount : "请求过于频繁";
                    throw new Exception($message, 101);
                }
            }
        }
    }

    /**
     * checkGet
     * @throws Exception
     */
    private function checkGet()
    {
        $debug = $this->wafConfig['debug'] ?? false;
        $check = $this->wafConfig['checkGet'] ?? false;
        if ($check) {
            $char = Utils::match($_GET, $this->getFilter);
            if ($char) {
                $message = $debug ? "get请求参数包含非法字符:{$char}" : "请求行为异常";
                throw new Exception($message, 102);
            }
        }
    }

    /**
     * checkPost
     * @throws Exception
     */
    private function checkPost()
    {
        $debug = $this->wafConfig['debug'] ?? false;
        $check = $this->wafConfig['checkPost'] ?? false;
        if ($check) {
            $char = Utils::match($_POST, $this->postFilter);
            if ($char) {
                $message = $debug ? "post请求参数包含非法字符:{$char}" : "请求行为异常";
                throw new Exception($message, 103);
            }
        }
    }

    /**
     * checkCookie
     * @throws Exception
     */
    private function checkCookie()
    {
        $debug = $this->wafConfig['debug'] ?? false;
        $check = $this->wafConfig['checkCookie'] ?? false;
        if ($check) {
            $char = Utils::match($_COOKIE, $this->cookieFilter);
            if ($char) {
                $message = $debug ? "cookie请求参数包含非法字符:{$char}" : "请求行为异常";
                throw new Exception($message, 104);
            }
        }
    }

    /**
     * checkUserAgent
     * @throws Exception
     */
    private function checkUserAgent()
    {
        $debug = $this->wafConfig['debug'] ?? false;
        $check = $this->wafConfig['checkUserAgent'] ?? false;
        if ($check) {
            $userAgent = empty($_SERVER['HTTP_USER_AGENT']) ? [] : ['HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT']];
            if (empty($userAgent)) {
                $message = $debug ? "useragent不得为空" : "请求环境异常";
                throw new Exception($message, 105);
            }
            $char = Utils::match($userAgent, $this->userAgentFilter);
            if ($char) {
                $message = $debug ? "useragent请求参数包含非法字符:{$char}" : "请求环境异常";
                throw new Exception($message, 106);
            }
        }
    }

    /**
     * checkProxyRequest
     * @return void
     * @throws Exception
     */
    private function checkProxyRequest()
    {
        $debug = $this->wafConfig['debug'] ?? false;
        $check = $this->wafConfig['checkProxyRequest'] ?? false;
        if ($check) {
            $message = $debug ? "请求网络为代理IP环境" : "请求网络不合法";
            if (Utils::isProxyRequest()) {
                throw new Exception($message, 107);
            }
            $clientIp = Utils::getClientIp();
            $clientRealIp = Utils::getClientRealIp();
            if ($clientIp != $clientRealIp) {
                throw new Exception($message, 107);
            }
        }
    }
}
